package cn.selfiot.iotman.view.workbench;

import cn.selfiot.iotman.MainController;
import cn.selfiot.iotman.core.I18N;
import cn.selfiot.iotman.core.StageController;
import cn.selfiot.iotman.core.StageManager;
import cn.selfiot.iotman.helper.MessageDialogHelper;
import cn.selfiot.iotman.helper.ProjectPersistHelper;
import cn.selfiot.iotman.model.mqtt.ProjectModel;
import cn.selfiot.iotman.model.mqtt.TopicModel;
import cn.selfiot.iotman.model.mqtt.messge.ActionItem;
import cn.selfiot.iotman.model.mqtt.messge.PublishMessageModel;
import cn.selfiot.iotman.model.mqtt.messge.SubscribeTopicModel;
import cn.selfiot.iotman.mqtt.MqttClientManager;
import cn.selfiot.iotman.view.ResourceConstants;
import cn.selfiot.iotman.view.console.Console;
import cn.selfiot.iotman.view.console.ConsoleParser;
import cn.selfiot.iotman.view.editor.JsonEditor;
import cn.selfiot.iotman.view.item.ListItem;
import com.panemu.tiwulfx.dialog.MessageDialog;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
import javafx.scene.Cursor;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.control.StatusBar;
import org.controlsfx.control.textfield.TextFields;
import org.eclipse.paho.client.mqttv3.*;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.model.StyleSpans;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.PrintStream;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * @author hugo
 * @since 2020/8/3
 */
public class MqttWorkbenchController extends StageController {
    private static final Logger LOGGER = LoggerFactory.getLogger(MqttWorkbenchController.class);
    @FXML
    public Label lbProjectName;
    @FXML
    public Label lbServerURI;
    @FXML
    public TabPane tpLeft;
    @FXML
    public VBox vbStatus;
    @FXML
    public Button btnConn;
    @FXML
    public Button btnDisconn;
    @FXML
    public Button btnBack;
    @FXML
    public Label lbActionMsg;
    @FXML
    public ToggleButton btnExpandRight;
    @FXML
    public SplitPane spBody;
    @FXML
    public TextField txtSearchPub;
    @FXML
    public TextField txtSearchSub;
    @FXML
    public Button btnAddPub;
    @FXML
    public Button btnAddSub;
    @FXML
    public ListView<ListItem> listPub;
    @FXML
    public ListView<ListItem> listSub;
    @FXML
    public TextField txtCurrTopic;
    @FXML
    public Button btnSend;
    @FXML
    public TabPane tpConsole;
    @FXML
    public Button btnClear;
    @FXML
    public HBox hbConsole;
    @FXML
    public HBox hbToggle;
    @FXML
    public HBox hbPubPayload;
    @FXML
    public Button btnClearReceived;
    @FXML
    public HBox hbSubMsg;
    @FXML
    public StatusBar statusBar;
    
    private CodeArea console;
    private ConsoleParser consoleParser;
    
    private ExecutorService executorService;
    private Future parsingFuture;
    
    private JsonEditor pubEditor;
    private JsonEditor subEditor;
    private ToggleButton btnQos0;
    private ToggleButton btnQos1;
    private ToggleButton btnQos2;
    private ToggleButton btnRetained;
    private SegmentedButton retainSegment;
    
    private StageManager stageManager;
    private ResourceBundle bundle;
    private ProjectModel mqttProject;
    private List<String> suggestions = new ArrayList<>();
    private List<PublishMessageModel> publishMessages = new ArrayList<>();
    private List<SubscribeTopicModel> subscribeTopics = new ArrayList<>();
    private ListItem actionItem;
    private String actionType = "PUB";
    private MqttClientManager mqttClientManager;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        stageManager = StageManager.getInstance();
        bundle = I18N.getBundle();
        consoleParser = new ConsoleParser();
        executorService = Executors.newFixedThreadPool(2);
        
        if (getParams() != null) {
            mqttProject = (ProjectModel) getParams().get("project");
            
            if (mqttProject.getPublishMessages() != null) {
                publishMessages = mqttProject.getPublishMessages();
            }
            if (mqttProject.getSubscribeTopics() != null) {
                subscribeTopics = mqttProject.getSubscribeTopics();
            }
        }
        
        tpLeft.getSelectionModel().selectedIndexProperty().addListener(
                (observable, oldValue, newValue) -> {
                    if (newValue.intValue() == 0) {
                        actionType = "PUB";
                        pubEditor.setDisable(false);
                        retainSegment.setVisible(true);
                        retainSegment.setManaged(true);
                    } else {
                        actionType = "SUB";
                        pubEditor.setDisable(true);
                        retainSegment.setVisible(false);
                        retainSegment.setManaged(false);
                    }
                });
        
        initView();
        initAction();
    }
    
    private void initView() {
        initProjectInfo();
        initSearchFiled();
        initListView();
        initQosBtn();
        
        initPubEditor();
        initConsole();
        initSubEditor();
    }
    
    private void initAction() {
        btnConn.setOnAction(event -> connection(mqttProject));
        btnDisconn.setOnAction(event -> disConnection());
        btnExpandRight.setOnAction(actionEvent -> {
            boolean selected = btnExpandRight.isSelected();
            if (selected) {
                expand();
            } else {
                collapse();
            }
        });
        btnBack.setOnAction(actionEvent -> back());
        btnClear.setOnAction(actionEvent -> clearLog());
        btnClearReceived.setOnAction(event -> clearReceived());
        btnSend.setOnAction(event -> sendAction());
    }
    
    private void initQosBtn() {
        // qos选择按钮
        btnQos0 = new ToggleButton("Qos 0");
        btnQos1 = new ToggleButton("Qos 1");
        btnQos2 = new ToggleButton("Qos 2");
        btnRetained = new ToggleButton("Retained");
        ToggleGroup qosGroup = new ToggleGroup();
        ToggleGroup retainGroup = new ToggleGroup();
        SegmentedButton qosSegment = new SegmentedButton(btnQos0, btnQos1, btnQos2);
        qosSegment.setToggleGroup(qosGroup);
        qosSegment.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
        
        retainSegment = new SegmentedButton(btnRetained);
        retainSegment.setToggleGroup(retainGroup);
        retainSegment.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
        hbToggle.setSpacing(4);
        hbToggle.getChildren().addAll(qosSegment, retainSegment);
        
    }
    
    private void initListView() {
        // 发布消息和订阅列表
        listPub.getItems().clear();
        for (PublishMessageModel pubMsg : publishMessages) {
            ListItem pubItem = new ListItem(pubMsg);
            listPub.getItems().add(pubItem);
        }
        listSub.getItems().clear();
        for (SubscribeTopicModel subscribeTopic : subscribeTopics) {
            ListItem pubItem = new ListItem(subscribeTopic);
            listSub.getItems().add(pubItem);
        }
        
        listPub.setContextMenu(createContextMenu(listPub));
        listSub.setContextMenu(createContextMenu(listSub));
        
        listPub.getSelectionModel().selectedItemProperty()
                .addListener((observable, oldValue, newValue) -> {
                    if (newValue != null) {
                        PublishMessageModel publishMessage =
                                (PublishMessageModel) newValue.getActionItem();
                        this.actionItem = newValue;
                        txtCurrTopic.setText(publishMessage.getTopic().getTopicString());
                        String payload = publishMessage.getPayload();
                        if (payload != null) {
                            pubEditor.getController().getCodeArea().replaceText(payload);
                        }
                        btnRetained.setSelected(publishMessage.getRetained());
                        int qos = publishMessage.getQos();
                        if (qos == 0) btnQos0.setSelected(true);
                        if (qos == 1) btnQos1.setSelected(true);
                        if (qos == 2) btnQos2.setSelected(true);
                    }
                });
        
        listSub.getSelectionModel().selectedItemProperty()
                .addListener((observable, oldValue, newValue) -> {
                    if (newValue != null) {
                        SubscribeTopicModel subscribeTopic =
                                (SubscribeTopicModel) newValue.getActionItem();
                        this.actionItem = newValue;
                        txtCurrTopic.setText(subscribeTopic.getTopic().getTopicString());
                        int qos = subscribeTopic.getQos();
                        if (qos == 0) btnQos0.setSelected(true);
                        if (qos == 1) btnQos1.setSelected(true);
                        if (qos == 2) btnQos2.setSelected(true);
                    }
                    
                });
    }
    
    private void initSearchFiled() {
        List<TopicModel> topics = mqttProject.getTopics();
        suggestions.clear();
        topics.forEach(topicModel -> {
            suggestions.add(topicModel.getTopicString());
        });
        btnAddPub.setOnAction(event -> addPubMessage());
        btnAddSub.setOnAction(event -> addSub());
        TextFields.bindAutoCompletion(txtSearchPub, suggestions.toArray());
        TextFields.bindAutoCompletion(txtSearchSub, suggestions.toArray());
        
        txtSearchPub.setOnKeyPressed(event -> {
            if (event.getCode().equals(KeyCode.ENTER)) {
                addPubMessage();
            }
        });
        txtSearchSub.setOnKeyPressed(event -> {
            if (event.getCode().equals(KeyCode.ENTER)) {
                addSub();
            }
        });
    }
    
    private void initProjectInfo() {
        //项目基本信息
        boolean enableSSL = mqttProject.getSsl() != null
                && mqttProject.getSsl().getEnable();
        String protocol = enableSSL ? "ssl://" : "tcp://";
        String hostUrl = protocol + mqttProject.getHostAddr() + ":"
                + mqttProject.getHostPort();
        lbProjectName.setText(mqttProject.getProjectName());
        lbServerURI.setText(hostUrl);
    }
    
    private void initConsole() {
        console = new CodeArea();
        console.prefWidthProperty().bind(hbConsole.widthProperty());
        console.setEditable(false);
        console.setWrapText(true);
        console.setCursor(Cursor.TEXT);
        console.textProperty().addListener(contentChangeListener);
        console.getStylesheets().clear();
        console.getStylesheets().add(consoleParser.getStylesheets());
        VBox.setVgrow(console, Priority.ALWAYS);
        
        VirtualizedScrollPane scroll = new VirtualizedScrollPane(console);
        VBox.setVgrow(scroll, Priority.ALWAYS);
        hbConsole.getChildren().add(scroll);
        
        PrintStream ps = new PrintStream(new Console(console), true);
        System.setOut(ps);
        System.setErr(ps);
    }
    
    private void initPubEditor() {
        pubEditor = new JsonEditor();
        VBox.setVgrow(pubEditor, Priority.ALWAYS);
        this.hbPubPayload.getChildren().add(pubEditor);
    }
    
    private void initSubEditor() {
        subEditor = new JsonEditor();
        VBox.setVgrow(subEditor, Priority.ALWAYS);
        subEditor.setOnlyEditor();
        subEditor.prefWidthProperty().bind(hbSubMsg.widthProperty());
        this.hbSubMsg.getChildren().add(subEditor);
    }
    
    private void sendAction() {
        if (mqttClientManager == null || !mqttClientManager.isConnected()) {
            updateActionMsg("fail", "Connect server first please.");
        } else {
            try {
                if ("PUB".equals(actionType)) {
                    PublishMessageModel publishMessage = new PublishMessageModel();
                    publishMessage.setId(actionItem.getId());
                    
                    TopicModel topicModel = new TopicModel();
                    topicModel.setTopicString(txtCurrTopic.getText());
                    publishMessage.setTopic(topicModel);
                    
                    int qos = 0;
                    if (btnQos0.isSelected()) qos = 0;
                    if (btnQos1.isSelected()) qos = 1;
                    if (btnQos2.isSelected()) qos = 2;
                    publishMessage.setQos(qos);
                    
                    publishMessage.setRetained(btnRetained.isSelected());
                    publishMessage.setPayload(pubEditor.getController().getCodeArea().getText());
                    
                    mqttClientManager.publishMessage(publishMessage);
                    
                    publishMessages.removeIf(s -> s.getId().equals(publishMessage.getId()));
                    publishMessages.add(publishMessage);
                    mqttProject.setPublishMessages(publishMessages);
                    
                    List<TopicModel> topics = mqttProject.getTopics();
                    boolean isExits = topics.stream().anyMatch(
                            s -> s.getTopicString().equals(publishMessage.getTopic().getTopicString()));
                    if (!isExits) {
                        mqttProject.getTopics().add(publishMessage.getTopic());
                    }
                    
                    this.saveProject(mqttProject);
                } else {
                    SubscribeTopicModel subscribeTopic = new SubscribeTopicModel();
                    TopicModel topicModel = new TopicModel();
                    topicModel.setTopicString(txtCurrTopic.getText());
                    subscribeTopic.setTopic(topicModel);
                    subscribeTopic.setId(actionItem.getActionItem().getId());
                    
                    int qos = 0;
                    if (btnQos0.isSelected()) qos = 0;
                    if (btnQos1.isSelected()) qos = 1;
                    if (btnQos2.isSelected()) qos = 2;
                    subscribeTopic.setQos(qos);
                    
                    mqttClientManager.subscribe(subscribeTopic, new IMqttActionListener() {
                        @Override
                        public void onSuccess(IMqttToken iMqttToken) {
                            updateActionMsg("success",
                                    "Successfully " +
                                            Arrays.toString(iMqttToken.getTopics()));
                            actionItem.setStyle("-fx-background-color: #53bddc;-fx-padding: 5;");
                        }
                        
                        @Override
                        public void onFailure(IMqttToken iMqttToken, Throwable throwable) {
                            updateActionMsg("success",
                                    "Subscribe Failed " +
                                            Arrays.toString(iMqttToken.getTopics()));
                        }
                    });
                    tpConsole.getSelectionModel().select(1);
                    
                    subscribeTopics.removeIf(s -> s.getId().equals(subscribeTopic.getId()));
                    subscribeTopics.add(subscribeTopic);
                    mqttProject.setSubscribeTopics(subscribeTopics);
                    
                    List<TopicModel> topics = mqttProject.getTopics();
                    boolean isExits = topics.stream().anyMatch(
                            s -> s.getTopicString().equals(subscribeTopic.getTopic().getTopicString()));
                    if (!isExits) {
                        mqttProject.getTopics().add(subscribeTopic.getTopic());
                    }
                    this.saveProject(mqttProject);
                }
                initSearchFiled();
            } catch (MqttException e) {
                LOGGER.error(e.getMessage());
            }
        }
    }
    
    private void back() {
        this.disConnection();
        stageManager = StageManager.getInstance();
        MainController controller = new MainController();
        controller.setStage(this.getStage());
        stageManager.loadStage(
                this.getStage(),
                controller,
                ResourceConstants.MAIN_VIEW,
                ResourceConstants.MAIN_RES,
                I18N.get("main.title"),
                null);
        this.getStage().show();
    }
    
    private void addPubMessage() {
        String topic = txtSearchPub.getText();
        TopicModel model = new TopicModel();
        model.setTopicString(topic);
        PublishMessageModel messageModel = new PublishMessageModel();
        messageModel.setTopic(model);
        messageModel.setId(UUID.randomUUID().toString());
        
        listPub.getItems().add(new ListItem(messageModel));
    }
    
    private void addSub() {
        String topic = txtSearchSub.getText();
        TopicModel model = new TopicModel();
        model.setTopicString(topic);
        SubscribeTopicModel subscribeTopic = new SubscribeTopicModel();
        subscribeTopic.setTopic(model);
        subscribeTopic.setId(UUID.randomUUID().toString());
        
        listSub.getItems().add(new ListItem(subscribeTopic));
    }
    
    private void connection(ProjectModel project) {
        Platform.runLater(() -> {
            mqttClientManager = MqttClientManager.getInstance(project);
            try {
                mqttClientManager.connect(mqttCallback);
            } catch (MqttException e) {
                MessageDialog.Answer answer = MessageDialogHelper.confirm(
                        this.getStage()
                        , bundle.getString("dialog.test.fail"));
                if (answer == MessageDialog.Answer.YES_OK
                        || answer == MessageDialog.Answer.CANCEL) {
                    try {
                        MqttClientManager.disconnect();
                    } catch (MqttException mqttException) {
                        LOGGER.error(mqttException.getMessage());
                    }
                }
            }
        });
    }
    
    private void disConnection() {
        Platform.runLater(() -> {
            try {
                MqttClientManager.disconnect();
                mqttClientManager = null;
                
                vbStatus.setStyle("-fx-background-color: #ff4081");
                btnConn.setDisable(false);
                btnDisconn.setDisable(true);
                
                updateActionMsg("success", "disconnected");
            } catch (MqttException mqttException) {
                LOGGER.error(mqttException.getMessage());
            }
        });
    }
    
    private void updateActionMsg(String type, String message) {
        Platform.runLater(() -> {
            lbActionMsg.setText(message);
            lbActionMsg.setStyle("-fx-font-size: 16px;-fx-font-weight: bold");
            if (type.equals("success")) lbActionMsg.setStyle("-fx-text-fill: #0E9654;-fx-fill: #0E9654");
            if (type.equals("fail")) lbActionMsg.setStyle("-fx-text-fill: #ff4081;-fx-fill: #ff4081");
        });
    }
    
    private void saveProject(ProjectModel project) {
        Platform.runLater(() -> {
            ProjectPersistHelper.saveProject(project);
        });
    }
    
    private void clearLog() {
        console.clear();
    }
    
    private void clearReceived() {
        subEditor.getController().getCodeArea().clear();
    }
    
    private void collapse() {
        spBody.setDividerPositions(0);
    }
    
    private void expand() {
        spBody.setDividerPositions(0.28);
    }
    
    private final ChangeListener<String> contentChangeListener = (obs, oldText, newText) -> {
        if (parsingFuture != null && !parsingFuture.isDone()) {
            parsingFuture.cancel(true);
        }
        
        parsingFuture = executorService.submit(() -> {
            final StyleSpans<Collection<String>> spans =
                    consoleParser.computeHighlighting(newText);
            Platform.runLater(() -> {
                try {
                    console.setStyleSpans(0, spans);
                } catch (Exception e) {
                    LOGGER.error(e.getMessage());
                }
            });
        });
    };
    
    @Override
    public void callback() {
    
    }
    
    private ContextMenu createContextMenu(ListView listView) {
        ContextMenu menu = new ContextMenu();
        
        MenuItem itemRemove = new MenuItem(bundle.getString("menu.topics.remove"));
        itemRemove.setAccelerator(KeyCombination.keyCombination("CTRL+X"));
        itemRemove.setOnAction(event -> {
            if (listView.getItems().size() > 0) {
                int selected = listView.getSelectionModel().getSelectedIndex();
                
                // 删除数据，更新存储文件
                ListItem selectedItem = (ListItem) listView.getItems().get(selected);
                ActionItem actionItem = selectedItem.getActionItem();
                if (actionType.equals("PUB")) {
                    publishMessages.remove(actionItem);
                    mqttProject.setPublishMessages(publishMessages);
                }
                if (actionType.equals("SUB")) {
                    subscribeTopics.remove(actionItem);
                    mqttProject.setSubscribeTopics(subscribeTopics);
                }
                this.saveProject(mqttProject);
                
                listView.getItems().remove(selected);
                
                event.consume();
            }
        });
        
        menu.getItems().addAll(itemRemove);
        return menu;
    }
    
    // MQTT监听并且接受消息
    private final MqttCallbackExtended mqttCallback = new MqttCallbackExtended() {
        @Override
        public void connectComplete(boolean reconnect, String serverURI) {
            Platform.runLater(() -> {
                vbStatus.setStyle("-fx-background-color: #0E9654");
                btnConn.setDisable(true);
                btnDisconn.setDisable(false);
                
                updateActionMsg("success", "connect successful!");
            });
        }
        
        @Override
        public void messageArrived(String topic, MqttMessage message) {
            Platform.runLater(() -> {
                subEditor.getController().getCodeArea().appendText("\r\n" + message.toString());
            });
        }
        
        @Override
        public void deliveryComplete(IMqttDeliveryToken token) {
            updateActionMsg("success", "message delivery complete,topics: " + Arrays.toString(token.getTopics()));
        }
        
        @Override
        public void connectionLost(Throwable arg0) {
            LOGGER.warn("connection lost,{}", arg0.getMessage());
            updateActionMsg("fail", "connection lost");
        }
    };
}
